﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

namespace MessageHandler
{
	public class Connection
	{
		private static IMessageHandler messageHandler;

		private Socket listeningSocket;
		private Configuration cfg;

		public Connection(IMessageHandler handler)
		{
			messageHandler = handler;
		}

		public void Connect(Configuration cfg)
		{
			if (cfg == null)
			{
				throw new ChatException("Keine Konfiguration zur Verbidung vorhanden.", "Fehler beim Verbinden");
			}
			this.cfg = cfg;

			if (cfg.IpAddress == null)
			{
				throw new ChatException("IP- Adresse nicht gesetzt.", "Fehler beim Verbinden");
			}

			if (cfg.Port <= 0)
			{
				throw new ChatException("Port nicht gesetzt.", "Fehler beim Verbinden");
			}

			OpenServerSocket();
		}

		private void OpenServerSocket()
		{
			IPAddress ipAddress = cfg.IpAddress;

			IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, (int)cfg.Port);

			// Listening Socket erstellen
			if (listeningSocket != null)
			{
				listeningSocket.Close();
			}
			listeningSocket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

			try
			{
				listeningSocket.Bind(ipEndPoint);
				listeningSocket.Listen(1);

				listeningSocket.BeginAccept(new AsyncCallback(AcceptCallback), listeningSocket);
			}
			catch
			{
				throw new ChatException("Fehler bei Erstellung eines Listening- Sockets", "Verbindungsfehler");
			}

		}

		public static void AcceptCallback(IAsyncResult asyncResult)
		{
			Socket listeningSocket = (Socket)asyncResult.AsyncState;
			try
			{
				Socket connSocket = listeningSocket.EndAccept(asyncResult);
				listeningSocket.BeginAccept(new AsyncCallback(AcceptCallback), listeningSocket);

				ReceiveStateObject state = new ReceiveStateObject();
				state.connectedSocket = connSocket;

				connSocket.BeginReceive(state.buffer, 0, ReceiveStateObject.BufferSize,
					0, new AsyncCallback(ReadCallback), state);
			}
			catch
			{
				messageHandler.HandleError(new ChatException("Fehler beim Akzeptieren der Clientverbindung", "Verbindungsfehler"));
			}
		}

		/// <summary>
		/// Wird aufgerufen, wenn Datenpakete vom Server ankommen
		/// </summary>
		/// <param name="asyncResult"></param>
		public static void ReadCallback(IAsyncResult asyncResult)
		{
			ReceiveStateObject state = (ReceiveStateObject)asyncResult.AsyncState;

			int bytesRead = 0;
			try
			{
				bytesRead = state.connectedSocket.EndReceive(asyncResult);
			}
			catch (SocketException e)
			{
				if (e.ErrorCode == 10053) // Data-Transmission-Timeout/ Protocol Error
				{
					// Connection durch den Anwender geschlossen
					messageHandler.HandleError(new ChatException("Data- Transmission- Timeout", "Verbindungsfehler"));
					state.connectedSocket.Close();
					return;
				}
				else if (e.ErrorCode == 995)
				{
					messageHandler.HandleError(new ChatException("Socket geschlossen", "Verbindungsfehler"));
					return;
				}
				else
				{
					// Connection durch die Gegenstelle geschlossen
					messageHandler.HandleError(new ChatException("Verbindung von Gegenstelle geschlossen", "Verbindungsfehler"));
					state.connectedSocket.Close();

					return;
				}
			}
			catch (System.ObjectDisposedException)
			{
				messageHandler.HandleError(new ChatException("Verbindung geschlossen", "Verbindungsfehler"));
				state.connectedSocket.Close();
				return;
			}

			if (bytesRead == 0)
			{
				// Connection durch die Gegenstelle geschlossen
				messageHandler.HandleError(new ChatException("Verbindung von Gegenstelle geschlossen", "Verbindungsfehler"));
				state.connectedSocket.Close();
				return;
			}

			if (state.messageLength == 0)
			{
				byte[] bytesAll = state.buffer;
				byte[] bytesLength = new byte[4];
				Array.Copy(bytesAll, bytesLength, 4);
				state.messageLength = BitConverter.ToInt32(bytesLength, 0);
				if (state.messageLength > 0)
				{
					state.completeMessage = new byte[state.messageLength];
					Array.Copy(bytesAll, 4, state.completeMessage, state.currentPosition, bytesRead - 4);
					state.currentPosition = bytesRead - 4;
				}
			}
			else
			{
				Array.Copy(state.buffer, 0, state.completeMessage, state.currentPosition, bytesRead);
				state.currentPosition += bytesRead;
			}

			if (state.currentPosition == state.completeMessage.Length)
			{
				messageHandler.HandleMessage(state.completeMessage, state.connectedSocket);
				state = new ReceiveStateObject() { connectedSocket = state.connectedSocket };
			}

			state.buffer = new byte[ReceiveStateObject.BufferSize];
			if (state.connectedSocket.Connected)
			{
				state.connectedSocket.BeginReceive(state.buffer, 0, ReceiveStateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
			}
		}

		public void Send(byte[] dataBytes, IEnumerable<Socket> socketList)
		{
			int streamLength = dataBytes.Length;
			byte[] lengthBytes = BitConverter.GetBytes(streamLength);
			byte[] byteAll = new byte[4 + streamLength];
			Array.Copy(lengthBytes, byteAll, 4);
			Array.Copy(dataBytes, 0, byteAll, 4, streamLength);

			foreach (Socket socket in socketList)
			{
				try
				{
					int byteSent = socket.Send(byteAll);
				}
				catch (NullReferenceException)
				{
					throw new ChatException("Konnte nicht senden, da nicht verbunden!", "Verbindungsfehler");
				}
				catch (SocketException)
				{
					this.Close(socket);
					messageHandler.HandleError(new ChatException("Konnte nicht senden!", "Socketfehler"));
				}
				catch (Exception)
				{
					messageHandler.HandleError(new ChatException("Konnte nicht senden!"));
					this.Close(socket);
				}
			}
		}

		public void Close(Socket connectedSocket)
		{
			if (connectedSocket != null && connectedSocket.Connected)
			{
				connectedSocket.Shutdown(SocketShutdown.Both);
				connectedSocket.Close();
				connectedSocket = null;
			}
			else
			{
				messageHandler.HandleError(new ChatException("Nichts zu schließen!"));
			}
		}

		// State object for reading client data asynchronously
		public class ReceiveStateObject
		{
			// Connected Socket
			public Socket connectedSocket = null;
			// Receive buffer.
			public const int BufferSize = 1024;
			public byte[] buffer = new byte[BufferSize];
			public byte[] completeMessage;
			public int currentPosition = 0;
			public int messageLength;

			public ReceiveStateObject()
			{
			}
		}
	}
}
